import "babel-polyfill";
import type from "../../type";
import Integer from "./integer";
import {gcd} from "../number-theory/gcd";
import {fastExponentation} from "../generic-algorithm";
import {ZeroDivisionError} from "../error";

export default class Fraction {
    constructor(numerator, denominator, coprime = false) {
        if (arguments.length === 0) {
            this.numerator = Integer.zero;
            this.denominator = Integer.one;
        } else if (arguments.length === 1) {
            if (numerator instanceof Fraction) {
                this.numerator = numerator.numerator;
                this.denominator = numerator.denominator;
            } else if (type(numerator) === "String") {
                let position = numerator.indexOf("/");
                if (position >= 0) {
                    let p = new Integer(numerator.slice(0, position).trim());
                    let q = new Integer(numerator.slice(position + 1).trim());
                    if (q.sign === 0) {
                        throw new ZeroDivisionError(`${p} / 0`);
                    } else if (q.sign < 0) {
                        p = Integer.negate(p);
                        q = Integer.negate(q);
                    }
                    let g = gcd(p, q);
                    this.numerator = Integer.exactDivide(p, g);
                    this.denominator = Integer.exactDivide(q, g);
                } else {
                    this.numerator = Integer.from(numerator);
                    this.denominator = Integer.one;
                }
            } else if (Integer.isInteger(numerator)) {
                this.numerator = Integer.from(numerator);
                this.denominator = Integer.one;
            } else {
                throw new TypeError("Invalid Number " + numerator);
            }
        } else {
            numerator = Integer.from(numerator);
            denominator = Integer.from(denominator);
            if (denominator.sign === 0) {
                throw new ZeroDivisionError(`${numerator} / 0`);
            } else if (denominator.sign < 0) {
                numerator = Integer.negate(numerator);
                denominator = Integer.negate(denominator);
            }
            if (!coprime) {
                let g = gcd(numerator, denominator);
                numerator = Integer.exactDivide(numerator, g);
                denominator = Integer.exactDivide(denominator, g);
            }
            this.numerator = numerator;
            this.denominator = denominator;
        }
        Object.freeze(this);
    }

    static isFraction(r) {
        return Integer.isInteger(r) || type(r) === "Fraction";
    }

    static from(r) {
        return r instanceof Fraction ? r : new Fraction(r);
    }

    get [Symbol.toStringTag]() {
        return "Fraction";
    }

    get sign() {
        return this.numerator.sign;
    }

    toString() {
        if (Integer.compare(this.denominator, Integer.one) === 0) {
            return `${this.numerator}`;
        } else {
            return `${this.numerator}/${this.denominator}`;
        }
    }

    static abs(r) {
        r = new Fraction(r);
        if (r.numerator.sign >= 0) {
            return r;
        } else {
            return new Fraction(Integer.abs(r.numerator), r.denominator, true);
        }
    }
    static negate(r) {
        r = new Fraction(r);
        if (r.numerator.sign === 0) {
            return r;
        } else {
            return new Fraction(Integer.negate(r.numerator), r.denominator, true);
        }
    }
    static inverse(r) {
        r = new Fraction(r);
        return new Fraction(r.denominator, r.numerator, true);
    }

    static eq(r1, r2) {
        r1 = new Fraction(r1);
        r2 = new Fraction(r2);
        return Integer.eq(r1.numerator, r2.numerator) && Integer.eq(r1.denominator, r2.denominator);
    }
    static compare(r1, r2) {
        r1 = new Fraction(r1);
        r2 = new Fraction(r2);
        if (r1.numerator.sign !== r2.numerator.sign) {
            return r1.numerator.sign - r2.numerator.sign;
        } else {
            return Integer.compare(
                Integer.multiply(r1.numerator, r2.denominator),
                Integer.multiply(r2.numerator, r1.denominator)
            );
        }
    }

    static add(r1, r2) {
        r1 = new Fraction(r1);
        r2 = new Fraction(r2);
        return new Fraction(Integer.add(
            Integer.multiply(r1.numerator, r2.denominator),
            Integer.multiply(r2.numerator, r1.denominator)
        ), Integer.multiply(r1.denominator, r2.denominator));
    }
    static subtract(r1, r2) {
        r1 = new Fraction(r1);
        r2 = new Fraction(r2);
        return new Fraction(Integer.subtract(
            Integer.multiply(r1.numerator, r2.denominator),
            Integer.multiply(r2.numerator, r1.denominator)
        ), Integer.multiply(r1.denominator, r2.denominator));
    }

    static multiply(r1, r2) {
        r1 = new Fraction(r1);
        r2 = new Fraction(r2);
        return new Fraction(
            Integer.multiply(r1.numerator, r2.numerator),
            Integer.multiply(r1.denominator, r2.denominator)
        );
    }
    static square(r) {
        r = new Fraction(r);
        return new Fraction(Integer.square(r.numerator), Integer.square(r.denominator), true);
    }
    static divide(r1, r2) {
        r1 = new Fraction(r1);
        r2 = new Fraction(r2);
        return new Fraction(
            Integer.multiply(r1.numerator, r2.denominator),
            Integer.multiply(r2.numerator, r1.denominator)
        );
    }

    static pow(base, exponent) {
        base = new Fraction(base);
        exponent = Integer.from(exponent);
        if (exponent.sign < 0) {
            base = Fraction.inverse(base);
        }
        return fastExponentation(base, exponent, {
            multiply: (r1, r2) => new Fraction(
                Integer.multiply(r1.numerator, r2.numerator),
                Integer.multiply(r1.denominator, r2.denominator),
                true
            ),
            square: Fraction.square,
            one: Fraction.one
        });
    }
};

Fraction.zero = new Fraction();
Fraction.one = new Fraction(1);
